Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Less alloc/wrapping/boxing for Query, Forms, Cookies #411

Closed
wants to merge 9 commits into from

Conversation

benaadams
Copy link
Contributor

  • Lower allocation for empty dictionaries of types HeaderDictionary, QueryCollection, FormCollection, RequestCookies
  • No allocation for empty Form parsing or Query parsing
  • Read/Write request data for middleware editing/processing (e.g. 3rd party auth that removes its data; or data transforms along the pipeline)
  • Removed IReadableStringCollection, ReadableStringCollection -> IQueryCollection, IRequestCookies, IFormCollection each implementing IDictionary<string, StringValues>
  • Added IQueryCollection, IRequestCookies, IFormCollection doesn't throw on get contract, non-boxing enumerator in Empty paths.
  • KeyValueAccumulator from class to struct
  • Removed a bunch of boxing paths
  • Reduced allocs in some paths (e.g. ToUriComponent or ParseQuery)
  • Changes to support non-boxing Equality for StringValues (see PR Allow non-boxing equality for StringValues dotnet/extensions#55)

Also cleaned up and added some intellisense code comments; but they still need work, so this is not "done"

@dnfclas
Copy link

dnfclas commented Sep 20, 2015

Hi @benaadams, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!

The agreement was validated by .NET Foundation and real humans are currently evaluating your PR.

TTYL, DNFBOT;

@benaadams
Copy link
Contributor Author

May need changes to Kestrel's custom IDictionaries dealing with empty keys
Also Content-Length bonding to header

@@ -12,7 +12,7 @@ public class HttpRequestFeature : IHttpRequestFeature
{
public HttpRequestFeature()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This implementation is used exclusively in unit tests, no optimizations required. We should add a doc comment to that effect.

@Tratcher
Copy link
Member

@benaadams What you're getting at with much of this is that our multiple levels of abstraction are inefiicent and we should scale back. That's legit, but I think we need to choose which collection we use carefully. We used Dictionaries internally because they were convenient storage, but as fowler pointed out we hate that their index throws, and they also have a significantly larger surface area than we need.

Rather than pushing IDictionary up to the surface APIs, let's try pushing a reduced IHeaderCollection down into the features/servers. we only really need IEnumerable<KeyValuePair<string, StringValues>>, Count, indexer, and ICollection<Keys>. @davidfowl, @lodejard, what do you think?

@Tratcher
Copy link
Member

The same goes for query, form, etc..

@davidfowl
Copy link
Member

@Tratcher we can't push the form and query etc down to the server but that's not what you mean I assume.

@Tratcher
Copy link
Member

@davidfowl Not down to the server, but down to their respective storage/features.

@benaadams
Copy link
Contributor Author

@Tratcher yes. However, after a large amount of changes, and some iterations to maintain a close analogue to flow and constraints, I mostly ended up in a similar place to where the code started; including wrapping on get for headers!

There are a few tweaks but they really end up independent to the abstraction changes (LazyDict and its inherited collections, KeyValueAccumulator). Which was't where I expected to be; and would have likely been the exact same place if I wasn't trying to avoid it. 😿

However, I did ponder along the journey...

Questions

  • Is wrapping the collections for no "throw on get" necessary? I completely agree with the reasons; but could this be a documented constraint for writing a custom server or implementing IHttpRequestFeature / IHttpResponseFeature? For example Kestrel's FrameHeaders collection will not throw on get and that will probably be the default for the majority of people. So is this overly defensive?

    Equally the default collections for forms and query are again provided so can be wrapped at that point (as per this PR with FormCollection : SafeLazyDictionary ignoring the inheritance exposure issue)

  • Is the request data being read-only desirable? With the request data being read-only this means middleware providers are more likely to resolve down to IHttpRequestFeature and wholesale replace the collection as they can't otherwise alter the data; making the defensive approach in the above question more necessary.

I had a few more, but I think they are artefacts of the above two and what implementation the answers encourage you down. Not necessarily looking for answers.

@Tratcher
Copy link
Member

IDictionary's contract is that the indexer throws, that's not something we can/should change in our stack as it would case a lot of unpleasant surprises. If we want a different semantic then we need a different contract / interface. I don't think we even need IDictionary, it was just the closest match to what wanted. Now that we're cleaning things up I think we can come up with a slimmed down type that has the semantics we want and use that in both the surface API (HttpRequest) and internally (IHttpRequestFeature).

The request data should be mutable. There are common scenarios where middleware will alter the request to meet downstream requirements like applying x-forwarded headers to their respective fields.

@benaadams
Copy link
Contributor Author

Different Contract makes sense; also with IgnoreCase on keys?

If you did something like this (inheritance-wise) everyone will want some:

interface IReadOnly<TKey, TValue>
{
    TValue Item { get; }
}

interface IReadWrite<TKey, TValue> : IReadOnly<TKey, TValue>
{
    new TValue Item { get; set; }
}

It seems to work, though you have to implement the get twice:

class Thing<TKey, TValue> : IReadWrite<TKey, TValue>
{
    public TValue Item
    {
        get
        {
            throw new NotImplementedException();
        }

        set
        {
            throw new NotImplementedException();
        }
    }

    TValue IReadOnly<TKey, TValue>.Item
    {
        get
        {
            return Item;
        }
    }
}

Then you can create a concrete type; use it read/write via interface and expose as readonly without any as casting between IReadWrite and IReadOnly only upcasting.

var thing = Thing();

thing["that"] = that;

IReadWrite iThing = thing;
iThing["other"] = other;

return (IReadOnly)iThing;

@benaadams benaadams changed the title [Design] Headers, Query, Forms, Cookies improvements [Discuss] Headers, Query, Forms, Cookies improvements Sep 21, 2015
@benaadams
Copy link
Contributor Author

Rebased rather than merged

@benaadams benaadams closed this Oct 31, 2015
@benaadams benaadams reopened this Oct 31, 2015
@dnfclas
Copy link

dnfclas commented Oct 31, 2015

Hi @benaadams, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!

The agreement was validated by .NET Foundation and real humans are currently evaluating your PR.

TTYL, DNFBOT;

@Tratcher
Copy link
Member

Tratcher commented Nov 2, 2015

@rynowak How badly will this break MVC?

@Tratcher Tratcher added this to the 1.0.0-rc2 milestone Nov 2, 2015
@Tratcher Tratcher self-assigned this Nov 2, 2015

switch (values.Count)
{
case 0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

values.Count == 0 was already checked in the prior if block. Might as well make this if..else if...else.

@rynowak
Copy link
Member

rynowak commented Nov 2, 2015

@Tratcher - I'd expect much more breakage in the tests which frequently new up collections for testing. We actually read these very few places in the product code.

}
}

StringValues IRequestCookieCollection.this[string key]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something is strange here, there shouldn't be two indexers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string and StringValues

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, this collection really is different from the others. It doesn't need to use StringValues anywhere, it only had that because of the old common IReadableStringCollection. We can just make this all <string, string> now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, makes things simpiler :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... how does that fit with RequestCookieFeature? https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.AspNet.Http/Features/RequestCookiesFeature.cs#L62

Multiple entries for the same value as via headers? Concat?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm; I think I've got it

@Tratcher
Copy link
Member

Tratcher commented Nov 2, 2015

This is pretty much done. I'll rebase and merge it when I get a chance later today. Any of the minor remaining comments you don't get to by then I'll just take care of. Thanks

@benaadams
Copy link
Contributor Author

Done, however quick check am returning string.Empty where a cookie doesn't exist which is "" rather than null; is this the expected behavior?

@Tratcher
Copy link
Member

Tratcher commented Nov 2, 2015

Should be fine. If they really care they can use TryGetValue

@Tratcher
Copy link
Member

Tratcher commented Nov 2, 2015

Rebased, squashed, merged.

@Tratcher Tratcher closed this Nov 2, 2015
@benaadams
Copy link
Contributor Author

👏 :)

@@ -25,7 +25,7 @@ public static class UriHelper
FragmentString fragment = new FragmentString())
{
string combinePath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/";
return combinePath + query + fragment;
return $"{combinePath}{query.ToString()}{fragment.ToString()}";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this has just 3 components, it would be more efficient reverting to the String.Concat() a+b+c format it was in before. While maintaining equal readability, it's ~3-4x faster on most machines (I've tested this on a variety locally and on the latest servers).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Send a PR 😄

NickCraver added a commit to NickCraver/HttpAbstractions that referenced this pull request Nov 3, 2015
This adds additional performance improvements (namely string.Concat
overloads) on top of aspnet#411.
@benaadams benaadams deleted the lazy-dict branch May 10, 2016 11:28
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.